#include "Common.h"
#include "dotconfpp.h"
#include <ace/OS_NS_stdlib.h>

#ifdef WIN32
#  define strcasecmp stricmp
#  include <io.h>
#else
#  include <unistd.h>
#  include <limits.h>
#  include <stdint.h>
#  include <strings.h>
#endif

#if !defined(R_OK)
#  define R_OK 04
#endif

DOTCONFDocumentNode::DOTCONFDocumentNode()
: previousNode(NULL), nextNode(NULL), parentNode(NULL), childNode(NULL),
values(NULL), valuesCount(0), name(NULL), lineNum(0),
fileName(NULL), closed(true)
{
}

DOTCONFDocumentNode::~DOTCONFDocumentNode()
{
    free(name);

    if (values != NULL)
    {
        for (int i = 0; i < valuesCount; ++i)
            free(values[i]);

        free(values);
    }
}

void DOTCONFDocumentNode::pushValue(char* _value)
{
    ++valuesCount;
    values = (char**)realloc(values, valuesCount * sizeof(char*));
    values[valuesCount - 1] = strdup(_value);
}

const char* DOTCONFDocumentNode::getValue(int index) const
{
    if (index >= valuesCount)
        return NULL;

    return values[index];
}

DOTCONFDocument::DOTCONFDocument(DOTCONFDocument::CaseSensitive caseSensitivity)
: mempool(NULL), curParent(NULL), curPrev(NULL), curLine(0), file(NULL), fileName(NULL)
{
    switch (caseSensitivity)
    {
        case CASESENSITIVE:
            cmp_func = strcmp;
            break;
        case CASEINSENSETIVE:
            cmp_func = strcasecmp;
            break;
    }

    mempool = new AsyncDNSMemPool(1024);
    mempool->initialize();
}

DOTCONFDocument::~DOTCONFDocument()
{
    for (NodeList::iterator i = nodeTree.begin(); i != nodeTree.end(); ++i)
        delete(*i);

    for (CharList::iterator i = requiredOptions.begin(); i != requiredOptions.end(); ++i)
        free(*i);

    for (CharList::iterator i = processedFiles.begin(); i != processedFiles.end(); ++i)
        free(*i);

    free(fileName);
    delete mempool;
}

int DOTCONFDocument::cleanupLine(char* line)
{
    char* start = line;
    char* bg = line;
    bool multiline = false;
    bool concat = false;
    char* word = NULL;

    if (!words.empty() && quoted)
        concat = true;

    while (*line)
    {
        if ((*line == '#' || *line == ';') && !quoted)
        {
            *bg = 0;
            if (strlen(start))
            {
                if (concat)
                {
                    word = (char*)mempool->Alloc(strlen(words.back()) + strlen(start) + 1);
                    strcpy(word, words.back());
                    strcat(word, start);
                    words.pop_back();
                    concat = false;
                }
                else
                    word = mempool->Strdup(start);

                words.push_back(word);
            }

            break;
        }

        if (*line == '=' && !quoted)
        {
            *line = ' ';
            continue;
        }

        // Allowing \" in there causes problems with directory paths
        // like "C:\MaNGOS\"
        //if(*line == '\\' && (*(line+1) == '"' || *(line+1) == '\'')){
        if (*line == '\\' && (*(line + 1) == '\''))
        {
            *bg++ = *(line + 1);
            line += 2;
            continue;
        }

        if (*line == '\\' && *(line + 1) == 'n')
        {
            *bg++ = '\n';
            line += 2;
            continue;
        }

        if (*line == '\\' && *(line + 1) == 'r')
        {
            *bg++ = '\r';
            line += 2;
            continue;
        }

        if (*line == '\\' && (*(line + 1) == '\n' || *(line + 1) == '\r'))
        {
            *bg = 0;
            if (strlen(start))
            {
                if (concat)
                {
                    word = (char*)mempool->Alloc(strlen(words.back()) + strlen(start) + 1);
                    strcpy(word, words.back());
                    strcat(word, start);
                    words.pop_back();
                    concat = false;
                }
                else
                    word = mempool->Strdup(start);

                words.push_back(word);
            }

            multiline = true;
            break;
        }

        if (*line == '"' || *line == '\'')
        {
            quoted = !quoted;
            ++line;
            continue;
        }

        if (isspace((unsigned char)*line) && !quoted)
        {
            *bg++ = 0;
            if (strlen(start))
            {
                if (concat)
                {
                    word = (char*)mempool->Alloc(strlen(words.back()) + strlen(start) + 1);
                    strcpy(word, words.back());
                    strcat(word, start);
                    words.pop_back();
                    concat = false;
                }
                else
                    word = mempool->Strdup(start);

                words.push_back(word);
            }

            start = bg;
            while (isspace((unsigned char)*++line))
            {
            }

            continue;
        }

        *bg++ = *line++;
    }

    if (quoted && !multiline)
    {
        error(curLine, fileName, "unterminated quote");
        return -1;
    }

    return multiline ? 1 : 0;
}

int DOTCONFDocument::parseLine()
{
    char* word = NULL;
    char* nodeName = NULL;
    char* nodeValue = NULL;
    DOTCONFDocumentNode* tagNode = NULL;
    bool newNode = false;

    for (CharList::iterator i = words.begin(); i != words.end(); ++i)
    {
        word = *i;

        if (*word == '<')
            newNode = true;

        if (newNode)
        {
            nodeValue = NULL;
            nodeName = NULL;
            newNode = false;
        }

        size_t wordLen = strlen(word);
        if (word[wordLen - 1] == '>')
        {
            word[wordLen - 1] = 0;
            newNode = true;
        }

        if (nodeName == NULL)
        {
            nodeName = word;
            bool closed = true;
            if (*nodeName == '<')
            {
                if (*(nodeName + 1) != '/')
                {
                    ++nodeName;
                    closed = false;
                }
                else
                {
                    NodeList::reverse_iterator itr = nodeTree.rbegin();
                    nodeName += 2;
                    for (; itr != nodeTree.rend(); ++itr)
                    {
                        if (!cmp_func(nodeName, (*itr)->name) && !(*itr)->closed)
                        {
                            (*itr)->closed = true;
                            curParent = (*itr)->parentNode;
                            curPrev = *itr;
                            break;
                        }
                    }

                    if(itr == nodeTree.rend())
                    {
                        error(curLine, fileName, "not matched closing tag </%s>", nodeName);
                        return -1;
                    }

                    continue;
                }
            }

            tagNode = new DOTCONFDocumentNode;
            tagNode->name = strdup(nodeName);
            tagNode->document = this;
            tagNode->fileName = processedFiles.back();
            tagNode->lineNum = curLine;
            tagNode->closed = closed;

            if(!nodeTree.empty())
            {
                DOTCONFDocumentNode* prev = nodeTree.back();
                if (prev->closed)
                {
                    curPrev->nextNode = tagNode;
                    tagNode->previousNode = curPrev;
                    tagNode->parentNode = curParent;
                }
                else
                {
                    prev->childNode = tagNode;
                    tagNode->parentNode = prev;
                    curParent = prev;
                }
            }

            nodeTree.push_back(tagNode);
            curPrev = tagNode;
        }
        else
        {
            nodeValue = word;
            tagNode->pushValue(nodeValue);
        }
    }

    return 0;
}

int DOTCONFDocument::parseFile(DOTCONFDocumentNode* _parent)
{
    char str[512];
    int ret = 0;
    curLine = 0;
    curParent = _parent;

    quoted = false;
    size_t slen = 0;

    while (fgets(str, 511, file))
    {
        ++curLine;
        slen = strlen(str);
        if (slen >= 510)
            error(curLine, fileName, "warning: line too long");

        if (str[slen - 1] != '\n')
        {
            str[slen] = '\n';
            str[slen + 1] = 0;
        }

        if ((ret = cleanupLine(str)) == -1)
            break;

        if (ret == 0)
        {
            if (!words.empty())
            {
                ret = parseLine();
                mempool->Free();
                words.clear();

                if(ret == -1)
                    break;
            }
        }
    }

    return ret;
}

int DOTCONFDocument::checkConfig(const NodeList::iterator& from)
{
    int ret = 0;

    DOTCONFDocumentNode* tagNode = NULL;
    int vi = 0;
    for (NodeList::iterator i = from; i != nodeTree.end(); ++i)
    {
        tagNode = *i;
        if (!tagNode->closed)
        {
            error(tagNode->lineNum, tagNode->fileName, "unclosed tag %s", tagNode->name);
            ret = -1;
            break;
        }

        vi = 0;
        while (vi < tagNode->valuesCount)
        {
            if (strstr(tagNode->values[vi], "${") && strchr(tagNode->values[vi], '}'))
            {
                ret = macroSubstitute(tagNode, vi);
                mempool->Free();
                if (ret == -1)
                    break;
            }

            ++vi;
        }

        if (ret == -1)
            break;
    }

    return ret;
}

int DOTCONFDocument::setContent(const char* _fileName)
{
    int ret = 0;
    char realpathBuf[MANGOS_PATH_MAX];

    if (ACE_OS::realpath(_fileName, realpathBuf) == NULL)
    {
        error(0, NULL, "realpath (%s) failed: %s", _fileName, strerror(errno));
        return -1;
    }

    fileName = strdup(realpathBuf);

    processedFiles.push_back(strdup(realpathBuf));

    if ((file = fopen(fileName, "r")) == NULL)
    {
        error(0, NULL, "failed to open file '%s': %s", fileName, strerror(errno));
        return -1;
    }

    // Try read utf8 header and skip it if exist
    unsigned int utf8header = 0;
    fgets((char*)&utf8header, 4, file); // Try read header
    if (utf8header != 0x00BFBBEF)       // If not exist
        fseek(file, 0, SEEK_SET);       // Reset read position

    ret = parseFile();

    fclose(file);

    if (!ret)
    {
        if ((ret = checkConfig(nodeTree.begin())) == -1)
            return -1;

        NodeList::iterator from;
        DOTCONFDocumentNode* tagNode = NULL;
        int vi = 0;
        for (NodeList::iterator i = nodeTree.begin(); i != nodeTree.end(); ++i)
        {
            tagNode = *i;
            if (!cmp_func("DOTCONFPPIncludeFile", tagNode->name))
            {
                vi = 0;
                while (vi < tagNode->valuesCount)
                {
                    if (access(tagNode->values[vi], R_OK) == -1)
                    {
                        error(tagNode->lineNum, tagNode->fileName, "%s: %s", tagNode->values[vi], strerror(errno));
                        return -1;
                    }

                    if (ACE_OS::realpath(tagNode->values[vi], realpathBuf) == NULL)
                    {
                        error(tagNode->lineNum, tagNode->fileName, "realpath (%s) failed: %s", tagNode->values[vi], strerror(errno));
                        return -1;
                    }

                    bool processed = false;
                    for (CharList::const_iterator itInode = processedFiles.begin(); itInode != processedFiles.end(); ++itInode)
                    {
                        if (!strcmp(*itInode, realpathBuf))
                        {
                            processed = true;
                            break;
                        }
                    }

                    if(processed)
                        break;

                    processedFiles.push_back(strdup(realpathBuf));

                    file = fopen(tagNode->values[vi], "r");
                    if(file == NULL)
                    {
                        error(tagNode->lineNum, fileName, "failed to open file '%s': %s", tagNode->values[vi], strerror(errno));
                        return -1;
                    }

                    fileName = strdup(realpathBuf);
                    from = nodeTree.end();
                    --from;

                    ret = parseFile();
                    fclose(file);

                    if (ret == -1)
                        return -1;
                    if (checkConfig(++from) == -1)
                        return -1;

                    ++vi;
                }
            }
        }

        if (!requiredOptions.empty())
            ret = checkRequiredOptions();
    }

    return ret;
}

int DOTCONFDocument::checkRequiredOptions()
{
    for (CharList::const_iterator ci = requiredOptions.begin(); ci != requiredOptions.end(); ++ci)
    {
        bool matched = false;
        for (NodeList::iterator i = nodeTree.begin(); i != nodeTree.end(); ++i)
        {
            if (!cmp_func((*i)->name, *ci))
            {
                matched = true;
                break;
            }
        }

        if(!matched)
        {
            error(0, NULL, "required option '%s' not specified", *ci);
            return -1;
        }
    }

    return 0;
}

void DOTCONFDocument::error(int lineNum, const char* fileName_, const char* fmt, ...)
{
    va_list args;
    va_start(args, fmt);

    size_t len = (lineNum != 0 ? strlen(fileName_) : 0) + strlen(fmt) + 50;
    char* buf = (char*)mempool->Alloc(len);

    if (lineNum)
        snprintf(buf, len, "DOTCONF++: file '%s', line %d: %s\n", fileName_, lineNum, fmt);
    else
        snprintf(buf, len, "DOTCONF++: %s\n", fmt);

    vfprintf(stderr, buf, args);

    va_end(args);
}

char* DOTCONFDocument::getSubstitution(char* macro, int lineNum)
{
    char* buf = NULL;
    char* variable = macro + 2;

    char* endBr = strchr(macro, '}');

    if (!endBr)
    {
        error(lineNum, fileName, "unterminated '{'");
        return NULL;
    }

    *endBr = 0;

    char* defaultValue = strchr(variable, ':');

    if (defaultValue)
    {
        *defaultValue++ = 0;
        if (*defaultValue != '-')
        {
            error(lineNum, fileName, "incorrect macro substitution syntax");
            return NULL;
        }

        ++defaultValue;
        if (*defaultValue == '"' || *defaultValue == '\'')
        {
            ++defaultValue;
            defaultValue[strlen(defaultValue) - 1] = 0;
        }
    }
    else
        defaultValue = NULL;

    char* subs = getenv(variable);
    if (subs)
        buf = mempool->Strdup(subs);
    else
    {
        NodeList::iterator i = nodeTree.begin();
        DOTCONFDocumentNode* tagNode = NULL;
        for (; i != nodeTree.end(); ++i)
        {
            tagNode = *i;
            if (!cmp_func(tagNode->name, variable))
            {
                if (tagNode->valuesCount != 0)
                {
                    buf = mempool->Strdup(tagNode->values[0]);
                    break;
                }
            }
        }

        if (i == nodeTree.end())
        {
            if (defaultValue)
                buf = mempool->Strdup(defaultValue);
            else
            {
                error(lineNum, fileName, "substitution not found and default value not given");
                return NULL;
            }
        }
    }

    return buf;
}

int DOTCONFDocument::macroSubstitute(DOTCONFDocumentNode* tagNode, int valueIndex)
{
    int ret = 0;
    char* macro = tagNode->values[valueIndex];
    size_t valueLen = strlen(tagNode->values[valueIndex]) + 1;
    char* value = (char*)mempool->Alloc(valueLen);
    char* v = value;
    char* subs = NULL;

    while (*macro)
    {
        if (*macro == '$' && *(macro + 1) == '{')
        {
            char* m = strchr(macro, '}');
            subs = getSubstitution(macro, tagNode->lineNum);
            if(subs == NULL)
            {
                ret = -1;
                break;
            }

            macro = m + 1;
            *v = 0;
            v = (char*)mempool->Alloc(strlen(value) + strlen(subs) + valueLen);
            strcpy(v, value);
            value = strcat(v, subs);
            v = value + strlen(value);
            continue;
        }

        *v++ = *macro++;
    }

    *v = 0;

    free(tagNode->values[valueIndex]);
    tagNode->values[valueIndex] = strdup(value);
    return ret;
}

const DOTCONFDocumentNode* DOTCONFDocument::getFirstNode() const
{
    if (!nodeTree.empty())
        return *nodeTree.begin();
    else
        return NULL;
}

const DOTCONFDocumentNode* DOTCONFDocument::findNode(const char* nodeName, const DOTCONFDocumentNode* parentNode, const DOTCONFDocumentNode* startNode) const
{
    NodeList::const_iterator i = nodeTree.begin();

    if (startNode == NULL)
        startNode = parentNode;

    if (startNode != NULL)
    {
        while (i != nodeTree.end() && (*i) != startNode)
            ++i;

        if (i != nodeTree.end())
            ++i;
    }

    for (; i != nodeTree.end(); ++i)
    {
        if ((*i)->parentNode != parentNode)
            continue;

        if (!cmp_func(nodeName, (*i)->name))
            return *i;
    }

    return NULL;
}

void DOTCONFDocument::setRequiredOptionNames(const char** requiredOptionNames)
{
    while (*requiredOptionNames)
    {
        requiredOptions.push_back(strdup(*requiredOptionNames));
        ++requiredOptionNames;
    }
}
